# (no interpreter)
# -*- coding: utf8 -*-
# 
# Copyright 2007 Joe Friedrichsen <pengi.films@gmail.com>
# 
# This file is part of bbgun.
# 
# bbgun is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# bbgun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with bbgun; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

"""Convert raw BBCode to xhtml

"""

import re

STRICT = 0

class BBCodeError(Exception): pass
class BadSubstitutionError(BBCodeError): pass
class MissingSubstitutionError(BBCodeError): pass

def to_xhtml(text, bbstrict=0):
    """Find bbCode tags and convert them to xhtml tags
    
    Keyword ags:
    text -- the text to be converted
    
    to_xhtml uses several hidden methods to individually handle the
    bbCode tags in the text. Changing the translation for one tag is quite
    simple this way. Please see the source code if you'd like to change or
    add a translation.
    
    """
    START_TAG = r"""
        \[          # all start tags begin with an open square bracket
        (\w+)       # GROUP: the name of the tag
        [:=]?       # there may be a colon or equal sign, but only one
        ([a-zA-Z0-9]{0,10})?    # GROUP: either an attr or hex session id
        [:=]?       # there may be another single colon or equal sign
        /*          # urls have slashes after their 'http:'
        "?          # quotes have double quote delims for the speaker's name
        (.*?)       # GROUP: the name, url, or session id
        "?          # quotes have a closing double quote as well
        \]          # all start tags finish with a closed square bracket
    """
    start_tag = re.compile(START_TAG, re.VERBOSE)
    start_tags = start_tag.findall(text)
    
    END_TAG = r"""
        \[/         # all end tags begin with an open square bracket and slash
        (\w+)       # GROUP: the name of the tag
        [:=]?       # there may be a single colon or equal sign
        ([a-zA-Z0-9]{0,10})?    # GROUP: either an attr or hex session id
        [:=]?       # there may be another singl colon or equal sign
        (.*?)       # GROUP: the hex session id
        \]          # all end tags finish with a closed square bracket
    """
    end_tag = re.compile(END_TAG, re.VERBOSE)
    end_tags = end_tag.findall(text)
    
    # Capture a pair of tags and GROUP the text between them
    #    NB: Doesn't find [b]overlapped [i]tag[/b] pairs[/i],
    #        but will return a matched pair (bold and bold, ital and ital...)
    TAG_PAIR = r"""
        \[           # all start tags begin with an open square bracket
        (?P<tag>\w+) # GROUP and NAME: the name of the tag
        [:=]?        # there may be a colon or equal sign, but only one
        ([a-zA-Z0-9]{0,10})?    # GROUP: either an attr or hex session id
        [:=]?        # there may be a single colon or equal sign
        /*           # urls have slashes after their 'http:'
        "?           # quotes have double quote delims for the speaker's name
        (.*?)        # GROUP: the name, url, or session id
        "?           # quotes have a closing double quote as well
        \]           # all start tags finish with a closed square bracket
        (.*?)        # GROUP: the content between the tags
        \[/          # all end tags begin with an open square bracket and slash
        ((?P=tag))   # GROUP: the name of the found tag
        [:=]?        # there may be a single colon or equal sign
        ([a-zA-Z0-9]{0,10})?    # GROUP: either an attr or hex session id
        [:=]?        # there may be another singl colon or equal sign
        (.*?)        # GROUP: the hex session id
        \]           # all end tags finish with a closed square bracket
    """
    tag_pair = re.compile(TAG_PAIR, re.VERBOSE | re.DOTALL)
    
    #for st in start_tags:
    #    print "Found start tag: %s" % str(st)
    #print
    #for end in end_tags:
    #    print "Found  end  tag: %s" % str(end)
    #for pair in tag_pairs:
    #    print "Found  tag pair: %s" % (str(pair))
    #print
    #print dir(tag_match)
    #print tag_match.groups()
    #print tag_match.group()
    
    # Replace pairs of bbCode tags until none are found
    num_pairs = 0
    i=1
    while tag_pair.search(text):
        num_pairs = num_pairs + len(tag_pair.findall(text))
        text = tag_pair.sub(_handle_unknowntag, text)
        print "to_xhtml: Iteration %d..." % i
        #print text
        i = i+1
    
    print "to_xhtml: %d start tags, %d end tags found; %d pairs replaced" % \
        (len(start_tags), len(end_tags), num_pairs)
    if len(end_tags) != num_pairs:
        print "Number of found end tags (%d) does not match number of replaced tag pairs (%d)!" % \
            (len(end_tags), num_pairs)
        #raise BadSubstitutionError, "Number of found end tags (%d) does not match number of replaced tag pairs (%d)!" % \
        #    (len(end_tags), num_pairs)
    return text

def _handle_unknowntag(match):
    """Determine the kind of tag in the match and call its handler
    
    Keyword args:
    match -- a match object with the following useful attribute:
        groups() -- a list of the groups in the matched string
    
    """
    name = match.groups()[0].lower()
    text = match.groups()[3]
    try:
        return globals()['_do_' + name](match)
    except KeyError:
        raise MissingSubstitutionError, "No method for tag: %s" % name
    
def _do_b(match):
    """Translate bold tags from bbCode to xhtml: <strong>
    
    bbCode has a simple syntax for bold text:
        [b:5570ec4908]a line of bold text[/b:5570ec4908]
    This gets matched as:
        ('b', '5570ec4908', '', 'a line of bold text', 'b', '5570ec4908', '')
    
    """
    return "<strong>%s</strong>" % match.groups()[3]

def _do_i(match):
    """Translate italic tags from bbCode to xhtml: <em>
    
    bbCode has a simple syntax for italic text:
        [i:c570ec49a8]a line of italic text[/i:c570ec49a8]
    This gets matched as:
        ('i', 'c570ec49a8', '', 'a line of italic text', 'i', 'c570ec49a8', '')
    
    """
    return "<em>%s</em>" % match.groups()[3]

def _do_u(match):
    """Translate underline tags from bbCode to xhtml
    
    NOTE: underlined text is not officially part of the strict
    xhtml standard. The closest approximation that WordPress supports
    is the <ins> tag. Underlines can also be done with:
    <span style="text-decoration: underline">underlined text</span> and
    will be translated that way if STRICT is 1.
    
    bbCode has a simple syntax for underlined text:
        [u:5570ec4908]underlined text[/u:5570ec4908]
    This gets matched as:
        ('u', '5570ec4908', '', 'underlined text', 'u', '5570ec4908', '')
    
    """
    if STRICT:
        return '<span style="text-decoration: underline">%s</span>' % match.groups()[3]
    else:
        return "<ins>%s</ins>" % match.groups()[3]

def _do_quote(match):
    """Translate quote tags from bbCode to xhtml: <blockquote>
    
    bbCode allows two kinds of quoting:
        [quote:6f3b341246="Mark Twain]If you tell the truth you don't have to remember anything.[/quote:6f3b341246]
        [quote:ab1d8bb0ab]Anonymous quote[/quote:ab1d8bb0ab]
    These get matched as:
        ('quote', '6f3b341246', 'Mark Twain', 'If you tell the truth you don't have to remember anything.', 'quote', '6f3b341246', '')
        ('quote', 'ab1d8bb0ab', '', 'Anonymous quote', 'quote', 'ab1d8bb0ab', '')
    
    """
    if match.groups()[2]:
        author = match.groups()[2]
    else:
        author = "Someone"
    quote = match.groups()[3]
    return "<blockquote><em>%(author)s...</em>\r\n%(quote)s</blockquote>" % locals()

def _do_code(match):
    """Translate code tags from bbCode to xhtml: <code>
    
    bbCode has a slightly complicated syntax for code:
        [code:1:da277f1524]import os[/code:1:da277f1524]
    This gets matched as:
        ('code', '1', 'da277f1524', 'import os', 'code', '1', 'da277f1524')

    """
    return "<code>%s</code>" % match.groups()[3]

def _do_list(match):
    """Translate list tags from bbCode to xhtml: <ol> or <ul>
    
    NOTE: WordPress doesn't support styled lists: it doesn't use the
    list-style-type attribute at all since it's not xhtml strict. However,
    if STRICT is 1, then styled lists will be written.
    
    bbCode supports three kinds of lists:
        [list:ab1d8bb0ab]I have a list[/list:u:ab1d8bb0ab]
        [list=1:b0063dfd91]I have a numbered list[/list:o:b0063dfd91]
        [list=a:d406323d91]I have a lettered list[/list:o:d406323d91]
    This gets matched as:
        ('list', 'ab1d8bb0ab', '', "I have a list.", 'list', 'u', 'ab1d8bb0ab')
        ('list', '1', 'b0063dfd91', 'I have a numbered list', 'list', 'o', 'b0063dfd91')
        ('list', 'a', 'd406323d91', 'I have a lettered list', 'list', 'o', 'd406323d91')
    
    """
    type = match.groups()[5]
    text = match.groups()[3]
    
    # Ordered lists are either numbered or lettered
    style = ''
    if type == 'o' and STRICT:
        if match.groups()[1] == 'a':
            style = " list-style-type: lower-alpha"
        else:
            style = " list-style-type: arabic-numbers"
    start_tag = type + 'l' + style
    end_tag = type + 'l'
    
    bullet_re = r"""
        \[              # a bullet starts with an open square bracket
        \*              # followed by an asterisk,
        :               # a colon,
        [a-f0-9]{10}    # and a 10-digit hex id
        \]              # finally, finish with a closed square bracket
    """
    bullet = re.compile(bullet_re, re.VERBOSE)
    
    # Open the first list item
    # If the text starts with a bullet, replace it with <li>
    if bullet.match(text):
        text = '<li>' + text[bullet.match(text).end():]
    else:
        text = '<li>' + text

    # Each subsequent bullet closes the previous one and starts its own
    text = bullet.sub('</li><li>', text)
    
    # Close the final list item
    text = text + '</li>'

    return "<%(start_tag)s>%(text)s</%(end_tag)s>" % locals()

def _do_img(match):
    """Translate image tags from bbCode to xhtml: <img />
    
    bbCode has a slightly complex syntax for images:
        [img:276a0f69c4]http://www.url.net/picture.jpg[/img:276a0f69c4]
    This gets matched as:
        ('img', '276a0f69c4', '', 'http://www.url.net/picture.jpg', 'img', '276a0f69c4', '')
    
    """
    url = match.groups()[3]
    name = re.sub('^.*/', '', url)
    return '<img src="%(url)s" alt="%(name)s" />' % locals()

def _do_url(match):
    """Translate url tags from bbCode to xhtml: <a href=...>
    
    bbCode allows two kinds of url tags:
        [url]http://www.dcpa.org[/url]
        [url=http://mypage.org/]Click here[/url]
    These are matched as:
        ('url', '', '', 'http://www.dcpa.org', 'url', '', '')
        ('url', 'http', 'mypage.org/', 'Click here', 'url', '', '')
    
    """
    if match.groups()[1]:
        protocol = match.groups()[1]
        address = match.groups()[2]
        url = "%(protocol)s://%(address)s" % locals()
    else:
        url = match.groups()[3]        
    anchor = match.groups()[3]
    return '<a href="%(url)s">%(anchor)s</a>' % locals()

def _do_color(match):
    """Translate color tags from bbCode to xhtml
    
    NOTE: colord text does not have an official xhtml standard tag. 
    Italicized text is used instead. If STRICT is 1, a span
    is used: <span style="color: $COLOR">text</span>
    
    bbCode has a slightly complex syntax for for color:
        [color=darkred:10831efb71]colored text[/color:10831efb71]
    This gets matched as:
        ('color', 'darkred', '10831efb71', 'colored text', 'color', '10831efb71', '')
    
    """
    if STRICT:
        return '<span style="color: %s">%s</span>' % (match.groups()[1], match.groups()[3])
    else:
        return "<em>%s</em>" % match.groups()[3]

def _do_size(match):
    """Translate size tags from bbCode to xhtml
    
    NOTE: sized text does not have an official xhtml standard tag.
    Bold text is used instead. If STRICT is 1, a span is used:
    <span style="font-size: $SIZE">text</span>
    
    bbCode has a slightly complex syntax for size:
        [size=18:]big text[/size:]
    This gets matched as:
        ('size', '18', '5570ec4908', 'big text', 'size', '5570ec4908', '')
    
    """
    if STRICT:
        return '<span syle="font-size: %s">%s</span>' % (match.groups()[1], match.groups()[3])
    else:
        return "<strong>%s</strong>" % match.groups()[3]

def _do_email(match):
    """Translate email tags from bbCode to xhtml <a href=mailto...>
    
    bbCode has a simple syntax for email:
        [email]mcp@tron.net[/email]
    This gets matched as:
        ('email', '', '', 'mcp@tron.net', 'email', '', '')

    """
    return '<a href="mailto:%s">%s</a>' % (match.groups()[3], match.groups()[3])

def _do_edit(match):
    """CUSTOM TAG
    
    """
    return "[%s]%s[%s]" % (match.groups()[0], match.groups()[3], match.groups()[4])
